S09-08 MySQL-项目:满汉楼
[TOC]
项目总览
系统定位
满汉楼餐饮管理系统是 Java SE 阶段通向企业级开发的经典综合实战项目。系统模拟真实餐厅的日常运营全流程,核心目的在于帮助开发者建立面向对象分层架构思维,掌握基于 Druid 连接池与 DBUtils 泛型反射 的通用 CRUD 设计。
[员工登录] ──> [查看餐桌] ──> [客户预订] ──> [开台点餐] ──> [明细核对] ──> [结账买单/清台]技术选型
| 技术维度 | 选型组件 | 核心作用 |
|---|---|---|
| 编程语言 | Java SE (JDK 8+) | 面向对象设计、集合框架、泛型与反射机制 |
| 数据存储 | MySQL 5.7+ (InnoDB) | 关系型数据存储、ACID 事务与行级锁 |
| 连接池 | Alibaba Druid | 数据库连接的高效复用与生命周期管理 |
| 持久层工具 | Apache Commons DBUtils | 简化 JDBC 代码,自动完成 SQL 结果集到 JavaBean 的 ORM 映射 |
| 数据安全 | MD5 散列摘要 | 员工密码脱敏加密存储与校验 |
工程目录结构
com.mhl/
├── domain/ # 实体层:与数据库表一一对应的 JavaBean 及多表 DTO
├── dao/ # 持久层:BasicDAO 通用父类及各业务 DAO 实现
├── service/ # 业务层:状态流转、业务校验与规则调度
├── view/ # 表现层:控制台多级循环菜单与人机交互
└── utils/ # 工具层:Druid 连接池管理、安全键盘输入工具基础底座
数据库连接池 JDBCUtilsByDruid
通过静态代码块解析 druid.properties 配置文件,构建全局唯一的连接池,提供安全的连接获取与资源释放机制。
# src/druid.properties
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/mhl?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username=root
password=root
initialSize=10
maxActive=30
maxWait=5000
minIdle=5package com.mhl.utils;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
public class JDBCUtilsByDruid {
private static DataSource ds;
static {
Properties properties = new Properties();
try {
InputStream in = JDBCUtilsByDruid.class.getClassLoader().getResourceAsStream("druid.properties");
properties.load(in);
ds = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
throw new RuntimeException("Druid 连接池初始化失败", e);
}
}
public static Connection getConnection() throws SQLException {
return ds.getConnection();
}
public static void close(ResultSet rs, Statement stmt, Connection conn) {
try {
if (rs != null) rs.close();
if (stmt != null) stmt.close();
if (conn != null) conn.close(); // 归还连接至池中
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}通用泛型持久层 BasicDAO<T>
利用 Apache Commons DBUtils 的 QueryRunner 与 Java 泛型反射(Class<T>),实现所有业务 DAO 零样板代码的单行、多行及标量查询。
package com.mhl.dao;
import com.mhl.utils.JDBCUtilsByDruid;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
public class BasicDAO<T> {
private QueryRunner qr = new QueryRunner();
// 通用 DML (INSERT, UPDATE, DELETE)
public int update(String sql, Object... params) {
Connection conn = null;
try {
conn = JDBCUtilsByDruid.getConnection();
return qr.update(conn, sql, params);
} catch (SQLException e) {
throw new RuntimeException("执行 DML 失败: " + sql, e);
} finally {
JDBCUtilsByDruid.close(null, null, conn);
}
}
// 查询返回多行 JavaBean 集合
public List<T> queryMulti(String sql, Class<T> clazz, Object... params) {
Connection conn = null;
try {
conn = JDBCUtilsByDruid.getConnection();
return qr.query(conn, sql, new BeanListHandler<>(clazz), params);
} catch (SQLException e) {
throw new RuntimeException("执行多行查询失败: " + sql, e);
} finally {
JDBCUtilsByDruid.close(null, null, conn);
}
}
// 查询返回单行 JavaBean 对象
public T querySingle(String sql, Class<T> clazz, Object... params) {
Connection conn = null;
try {
conn = JDBCUtilsByDruid.getConnection();
return qr.query(conn, sql, new BeanHandler<>(clazz), params);
} catch (SQLException e) {
throw new RuntimeException("执行单行查询失败: " + sql, e);
} finally {
JDBCUtilsByDruid.close(null, null, conn);
}
}
// 查询返回单行单列的标量值 (COUNT/SUM/MAX等)
public Object queryScalar(String sql, Object... params) {
Connection conn = null;
try {
conn = JDBCUtilsByDruid.getConnection();
return qr.query(conn, sql, new ScalarHandler<>(), params);
} catch (SQLException e) {
throw new RuntimeException("执行标量查询失败: " + sql, e);
} finally {
JDBCUtilsByDruid.close(null, null, conn);
}
}
}主程序
package com.mhl.view;
import com.mhl.domain.*;
import com.mhl.service.*;
import com.mhl.utils.Utility;
import java.util.List;
public class MHLView {
private boolean loop = true;
private EmployeeService employeeService = new EmployeeService();
private DiningTableService diningTableService = new DiningTableService();
private MenuService menuService = new MenuService();
private BillService billService = new BillService();
public static void main(String[] args) {
new MHLView().mainMenu();
}
public void mainMenu() {
while (loop) {
System.out.println("\n================= 满汉楼网络餐饮系统 =================");
System.out.println(" 1 登录满汉楼");
System.out.println(" 2 退出系统");
System.out.print("请输入你的选择: ");
String key = Utility.readString(1);
switch (key) {
case "1":
System.out.print("请输入员工号: ");
String empId = Utility.readString(50);
System.out.print("请输入密 码: ");
String pwd = Utility.readString(50);
Employee emp = employeeService.getEmployeeByIdAndPwd(empId, pwd);
if (emp != null) {
System.out.println("\n============ 登录成功 [" + emp.getName() + " - " + emp.getJob() + "] ============");
secondaryMenu();
} else {
System.out.println("========== 登录失败:工号或密码错误 ==========");
}
break;
case "2":
loop = false;
System.out.println("退出满汉楼系统成功!");
break;
default:
System.out.println("输入有误,请重新输入!");
}
}
}
private void secondaryMenu() {
boolean secLoop = true;
while (secLoop) {
System.out.println("================= 满汉楼二级主菜单 =================");
System.out.println(" 1 显示餐桌状态");
System.out.println(" 2 预订餐桌");
System.out.println(" 3 显示所有菜品");
System.out.println(" 4 点餐服务");
System.out.println(" 5 查看账单");
System.out.println(" 6 结账");
System.out.println(" 9 退出系统");
System.out.print("请输入你的选择: ");
String key = Utility.readString(1);
switch (key) {
case "1":
showDiningTable();
break;
case "2":
orderDiningTable();
break;
case "3":
showMenu();
break;
case "4":
orderMenu();
break;
case "5":
showBill();
break;
case "6":
payBill();
break;
case "9":
secLoop = false;
break;
default:
System.out.println("输入指令不存在,请重新输入!");
}
}
}
private void showDiningTable() {
List<DiningTable> list = diningTableService.list();
System.out.println("餐桌编号 餐桌状态 预订人 预订电话");
for (DiningTable dt : list) {
System.out.println(dt.getId() + " " + dt.getState() + " " + dt.getOrderName() + " " + dt.getOrderTel());
}
}
private void orderDiningTable() {
System.out.println("\n================= 预订餐桌 =================");
System.out.print("请选择预订的餐桌编号(-1退出): ");
int orderId = Utility.readInt();
if (orderId == -1) return;
char key = Utility.readConfirmSelection();
if (key == 'Y') {
DiningTable dt = diningTableService.getDiningTableById(orderId);
if (dt == null) {
System.out.println("该餐桌不存在!");
return;
}
if (!"空闲".equals(dt.getState())) {
System.out.println("该餐桌已被预订或正在就餐中!");
return;
}
System.out.print("预订人姓名: ");
String orderName = Utility.readString(50);
System.out.print("预订人电话: ");
String orderTel = Utility.readString(20);
if (diningTableService.orderDiningTable(orderId, orderName, orderTel)) {
System.out.println("============= 预订餐桌成功 =============");
}
}
}
private void showMenu() {
List<Menu> list = menuService.list();
System.out.println("菜品编号 菜品名 类别 价格");
for (Menu menu : list) {
System.out.println(menu.getId() + " " + menu.getName() + " " + menu.getType() + " " + menu.getPrice());
}
}
private void orderMenu() {
System.out.println("\n================= 点餐服务 =================");
System.out.print("请输入点餐的桌号(-1退出): ");
int tableId = Utility.readInt();
if (tableId == -1) return;
DiningTable dt = diningTableService.getDiningTableById(tableId);
if (dt == null) {
System.out.println("餐桌号不存在!");
return;
}
System.out.print("请输入菜品编号(-1退出): ");
int menuId = Utility.readInt();
if (menuId == -1) return;
Menu menu = menuService.getMenuById(menuId);
if (menu == null) {
System.out.println("菜品编号不存在!");
return;
}
System.out.print("请输入点餐份数(-1退出): ");
int nums = Utility.readInt();
if (nums == -1) return;
if (billService.orderMenu(menuId, nums, tableId)) {
System.out.println("============= 点餐成功并更新餐桌状态 =============");
}
}
private void payBill() {
System.out.println("\n================= 结账买单 =================");
System.out.print("请选择结账的餐桌编号(-1退出): ");
int tableId = Utility.readInt();
if (tableId == -1) return;
DiningTable dt = diningTableService.getDiningTableById(tableId);
if (dt == null) {
System.out.println("结账餐桌不存在!");
return;
}
if (!billService.hasUnpaidBill(tableId)) {
System.out.println("该餐桌没有未结账单!");
return;
}
System.out.print("结账方式 (现金/微信/支付宝) 回车退出: ");
String payMode = Utility.readString(20, "");
if ("".equals(payMode)) return;
char confirm = Utility.readConfirmSelection();
if (confirm == 'Y') {
if (billService.payBill(tableId, payMode)) {
System.out.println("============= 结账成功,已释放餐桌 =============");
}
}
}
private void showBill() {
List<MultiTableBean> multiTableBeans = billService.listMultiTable();
System.out.println("\n编号 菜品号 菜名 单价 份数 总金额 桌号 日期 状态");
for (MultiTableBean b : multiTableBeans) {
System.out.println(b.getId() + " " + b.getMenuId() + " " + b.getName() + " " +
b.getPrice() + " " + b.getNums() + " " + b.getMoney() + " " +
b.getDiningTableId() + " " + b.getBillDate() + " " + b.getState());
}
}
}员工登录
业务流程与表设计
系统一级入口。员工输入工号和明文密码,服务端通过 MD5 散列匹配数据库,验证通过后开放二级主操作菜单。

| 字段名 | 类型 | 约束 | 业务说明 |
|---|---|---|---|
id | INT | PRIMARY KEY, AUTO_INCREMENT | 员工唯一 ID |
empId | VARCHAR(50) | UNIQUE, NOT NULL | 员工工号(如 "6666") |
pwd | CHAR(32) | NOT NULL | 登录密码(MD5 密文存储) |
name | VARCHAR(50) | NOT NULL | 员工姓名 |
job | VARCHAR(50) | NOT NULL | 岗位(大堂经理/服务员/前台) |
实体与持久层
// 实体类 Employee.java
package com.mhl.domain;
public class Employee {
private Integer id;
private String empId;
private String pwd;
private String name;
private String job;
public Employee() {}
// Getters and Setters...
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getEmpId() { return empId; }
public void setEmpId(String empId) { this.empId = empId; }
public String getPwd() { return pwd; }
public void setPwd(String pwd) { this.pwd = pwd; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getJob() { return job; }
public void setJob(String job) { this.job = job; }
}// 数据访问 EmployeeDAO.java
package com.mhl.dao;
import com.mhl.domain.Employee;
public class EmployeeDAO extends BasicDAO<Employee> {}鉴权业务与控制台交互
// 业务层 EmployeeService.java
package com.mhl.service;
import com.mhl.dao.EmployeeDAO;
import com.mhl.domain.Employee;
public class EmployeeService {
private EmployeeDAO employeeDAO = new EmployeeDAO();
// 校验工号与密码 (SQL 中使用 MD5 函数直接比对)
public Employee getEmployeeByIdAndPwd(String empId, String pwd) {
String sql = "SELECT * FROM employee WHERE empId = ? AND pwd = MD5(?)";
return employeeDAO.querySingle(sql, Employee.class, empId, pwd);
}
}- 表现层交互逻辑:
- 用户在控制台选择
1 登录满汉楼; - 调用
Utility.readString()读取工号与密码; - 调用
EmployeeService.getEmployeeByIdAndPwd(),校验成功显示欢迎信息并跳转二级主菜单,失败则提示重试。
- 用户在控制台选择
餐桌预订
业务流程与表设计
餐桌是餐厅运营的核心实体,严格遵循 三态状态机 转移规则:
[空闲] ──(客户预订)──> [已预订] ──(开台点餐)──> [就餐中] ──(结账买单)──> [空闲 (清空预订人)]| 字段名 | 类型 | 约束 | 业务说明 |
|---|---|---|---|
id | INT | PRIMARY KEY, AUTO_INCREMENT | 餐桌物理编号 (1~N 号桌) |
state | VARCHAR(20) | DEFAULT '空闲' | 餐桌当前状态 (空闲/已预订/就餐中) |
orderName | VARCHAR(50) | DEFAULT '' | 预订人姓名 |
orderTel | VARCHAR(20) | DEFAULT '' | 预订人联系电话 |
实体与持久层
// 实体类 DiningTable.java
package com.mhl.domain;
public class DiningTable {
private Integer id;
private String state;
private String orderName;
private String orderTel;
public DiningTable() {}
// Getters and Setters...
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getState() { return state; }
public void setState(String state) { this.state = state; }
public String getOrderName() { return orderName; }
public void setOrderName(String orderName) { this.orderName = orderName; }
public String getOrderTel() { return orderTel; }
public void setOrderTel(String orderTel) { this.orderTel = orderTel; }
}// 数据访问 DiningTableDAO.java
package com.mhl.dao;
import com.mhl.domain.DiningTable;
public class DiningTableDAO extends BasicDAO<DiningTable> {}预订业务与控制台交互
// 业务层 DiningTableService.java
package com.mhl.service;
import com.mhl.dao.DiningTableDAO;
import com.mhl.domain.DiningTable;
import java.util.List;
public class DiningTableService {
private DiningTableDAO diningTableDAO = new DiningTableDAO();
// 1. 查询所有餐桌列表
public List<DiningTable> list() {
return diningTableDAO.queryMulti("SELECT * FROM diningTable", DiningTable.class);
}
// 2. 根据 ID 获取餐桌详情
public DiningTable getDiningTableById(int id) {
return diningTableDAO.querySingle("SELECT * FROM diningTable WHERE id = ?", DiningTable.class, id);
}
// 3. 预订餐桌 (更新状态并登记客户信息)
public boolean orderDiningTable(int id, String orderName, String orderTel) {
String sql = "UPDATE diningTable SET state = '已预订', orderName = ?, orderTel = ? WHERE id = ?";
return diningTableDAO.update(sql, orderName, orderTel, id) > 0;
}
// 4. 更新餐桌就餐状态
public boolean updateDiningTableState(int id, String state) {
String sql = "UPDATE diningTable SET state = ? WHERE id = ?";
return diningTableDAO.update(sql, state, id) > 0;
}
// 5. 结账清台 (重置为空闲并清空客户信息)
public boolean updateDiningTableToFree(int id) {
String sql = "UPDATE diningTable SET state = '空闲', orderName = '', orderTel = '' WHERE id = ?";
return diningTableDAO.update(sql, id) > 0;
}
}- 表现层交互逻辑:
显示餐桌:遍历
diningTableService.list(),控制台表格化输出。
预订餐桌:输入桌号 → 校验该桌状态是否为
"空闲"→ 输入姓名与手机号 → 确认后更新数据库。
菜谱管理
业务流程与表设计
维护门店在售菜品清单与单价,供点餐时选择与核价。
| 字段名 | 类型 | 约束 | 业务说明 |
|---|---|---|---|
id | INT | PRIMARY KEY, AUTO_INCREMENT | 菜品编号 |
name | VARCHAR(50) | NOT NULL | 菜品名称(如“佛跳墙”、“东坡肉”) |
type | VARCHAR(50) | NOT NULL | 菜品类别(热菜/汤品/主食) |
price | DOUBLE(10,2) | NOT NULL | 菜品单价 (元) |
实体与持久层
// 实体类 Menu.java
package com.mhl.domain;
public class Menu {
private Integer id;
private String name;
private String type;
private Double price;
public Menu() {}
// Getters and Setters...
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }
}// 数据访问 MenuDAO.java
package com.mhl.dao;
import com.mhl.domain.Menu;
public class MenuDAO extends BasicDAO<Menu> {}菜谱查询与控制台交互
// 业务层 MenuService.java
package com.mhl.service;
import com.mhl.dao.MenuDAO;
import com.mhl.domain.Menu;
import java.util.List;
public class MenuService {
private MenuDAO menuDAO = new MenuDAO();
// 获取全量菜品列表
public List<Menu> list() {
return menuDAO.queryMulti("SELECT * FROM menu", Menu.class);
}
// 根据菜品 ID 获取菜品详情 (获取单价)
public Menu getMenuById(int id) {
return menuDAO.querySingle("SELECT * FROM menu WHERE id = ?", Menu.class, id);
}
}表现层交互逻辑:用户选择
3 显示所有菜品,调用menuService.list()格式化打印菜品编号、名称、类别及单价。
开台点餐
业务流程与表设计
核心经营业务:输入餐桌号、菜品号与数量后,系统自动生成 UUID 流水号,计算总金额并插入 bill 表,同时联动将该餐桌状态更改为 "就餐中"。
| 字段名 | 类型 | 约束 | 业务说明 |
|---|---|---|---|
id | INT | PRIMARY KEY, AUTO_INCREMENT | 账单流水主键 |
billId | VARCHAR(64) | NOT NULL | 全局唯一业务订单号 (UUID) |
menuId | INT | NOT NULL | 菜品编号 (关联 menu.id) |
nums | INT | NOT NULL | 点餐份数 |
money | DOUBLE(10,2) | NOT NULL | 该项小计 (nums * menu.price) |
diningTableId | INT | NOT NULL | 就餐桌号 (关联 diningTable.id) |
billDate | DATETIME | NOT NULL | 点餐时间戳 |
state | VARCHAR(50) | NOT NULL | 状态:未结账/现金/微信/支付宝 |
实体与持久层
注意事项:数据库日期类型映射
使用 MySQL 8 查询
DATETIME或TIMESTAMP字段时,驱动可能返回java.time.LocalDateTime。Java 实体类中的日期属性类型必须与数据库返回类型兼容。
如果实体类使用
java.util.Date,可能出现:Cannot set date: incompatible types推荐将实体类中的日期字段统一改为
LocalDateTime:javaprivate LocalDateTime date; public LocalDateTime getDate() { return date; } public void setDate(LocalDateTime date) { this.date = date; }
// 实体类 Bill.java
package com.mhl.domain;
import java.time.LocalDateTime;
public class Bill {
private Integer id;
private String billId;
private Integer menuId;
private Integer nums;
private Double money;
private Integer diningTableId;
private LocalDateTime billDate;
private String state;
public Bill() {}
// Getters and Setters...
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getBillId() { return billId; }
public void setBillId(String billId) { this.billId = billId; }
public Integer getMenuId() { return menuId; }
public void setMenuId(Integer menuId) { this.menuId = menuId; }
public Integer getNums() { return nums; }
public void setNums(Integer nums) { this.nums = nums; }
public Double getMoney() { return money; }
public void setMoney(Double money) { this.money = money; }
public Integer getDiningTableId() { return diningTableId; }
public void setDiningTableId(Integer diningTableId) { this.diningTableId = diningTableId; }
public LocalDateTime getBillDate() { return billDate; }
public void setBillDate(LocalDateTime billDate) { this.billDate = billDate; }
public String getState() { return state; }
public void setState(String state) { this.state = state; }
}// 数据访问 BillDAO.java
package com.mhl.dao;
import com.mhl.domain.Bill;
public class BillDAO extends BasicDAO<Bill> {}点餐业务逻辑与交互
注意事项:UUID 写入数据库
UUID.randomUUID()返回的是UUID对象,不是字符串。数据库中的
bill_id通常为VARCHAR类型,不能直接接收UUID对象。写入数据库前必须转换为字符串:
javaString billId = UUID.randomUUID().toString(); // 或者 bd.update(sql, billId.toString(), ...);
bill_id字段长度应至少为VARCHAR(36)。避免使用不指定字段名的
INSERT INTO bill VALUES (...),建议明确列出字段名,防止表结构顺序变化导致插入错误。
// 业务层 BillService.java (点餐部分)
package com.mhl.service;
import com.mhl.dao.BillDAO;
import java.util.UUID;
public class BillService {
private BillDAO billDAO = new BillDAO();
private MenuService menuService = new MenuService();
private DiningTableService diningTableService = new DiningTableService();
public boolean orderMenu(int menuId, int nums, int diningTableId) {
// 1. 生成全局唯一订单号
String billId = UUID.randomUUID().toString();
// 2. 计算金额小计
double money = menuService.getMenuById(menuId).getPrice() * nums;
// 3. 写入账单表
String insertSql = "INSERT INTO bill VALUES(NULL, ?, ?, ?, ?, ?, NOW(), '未结账')";
int affected = billDAO.update(insertSql, billId, menuId, nums, money, diningTableId);
if (affected <= 0) return false;
// 4. 联动更新餐桌状态为 '就餐中'
return diningTableService.updateDiningTableState(diningTableId, "就餐中");
}
}表现层交互逻辑:
- 用户选择
4 点餐服务; - 校验餐桌号存在性;
- 校验菜品编号存在性;
- 输入点餐数量,调用
billService.orderMenu()完成点餐并更改桌态; - 点餐成功后生成账单。

- 用户选择
账单查询
业务需求与多表 DTO 设计
单表 bill 仅记录了 menuId,无法直观展示菜名与单价。通过多表联查(bill JOIN menu),将结果映射到复合传输对象 MultiTableBean。
// 复合传输实体 MultiTableBean.java
package com.mhl.domain;
import java.util.Date;
public class MultiTableBean {
private Integer id;
private String billId;
private Integer menuId;
private Integer nums;
private Double money;
private Integer diningTableId;
private Date billDate;
private String state;
// 扩展字段 (来自 menu 表)
private String name;
private Double price;
public MultiTableBean() {}
// Getters and Setters...
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getBillId() { return billId; }
public void setBillId(String billId) { this.billId = billId; }
public Integer getMenuId() { return menuId; }
public void setMenuId(Integer menuId) { this.menuId = menuId; }
public Integer getNums() { return nums; }
public void setNums(Integer nums) { this.nums = nums; }
public Double getMoney() { return money; }
public void setMoney(Double money) { this.money = money; }
public Integer getDiningTableId() { return diningTableId; }
public void setDiningTableId(Integer diningTableId) { this.diningTableId = diningTableId; }
public Date getBillDate() { return billDate; }
public void setBillDate(Date billDate) { this.billDate = billDate; }
public String getState() { return state; }
public void setState(String state) { this.state = state; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }
}多表持久层与业务实现
注意事项:数据库字段与 Java 属性映射
- 数据库字段通常使用下划线命名,例如:
menu_id、dining_table_id。- Java 实体类通常使用驼峰命名,例如:
menuId、diningTableId。- 使用
BeanListHandler等工具自动封装查询结果时,不能保证自动将下划线字段转换为驼峰属性。- 若字段名与实体属性名不一致,实体对应属性可能为
null,即使数据库中实际有数据。解决方式:在 SQL 查询中为字段显式设置别名,使其与 Java 属性名一致。
javaString sql = "SELECT id, bill_id AS billId, menu_id AS menuId, num, amount, " + "dining_table_id AS diningTableId, date, state FROM bill;";这样可确保:
bill_id→billIdmenu_id→menuIddining_table_id→diningTableId
// MultiTableDAO.java
package com.mhl.dao;
import com.mhl.domain.MultiTableBean;
public class MultiTableDAO extends BasicDAO<MultiTableBean> {}// 业务层 BillService.java (查询部分)
public List<MultiTableBean> listMultiTable() {
String sql = """
SELECT bill.*, menu.name, menu.price
FROM bill
LEFT JOIN menu ON bill.menuId = menu.id
ORDER BY bill.id DESC
""";
return new MultiTableDAO().queryMulti(sql, MultiTableBean.class);
}表现层交互逻辑:用户选择
5 查看账单,调用billService.listMultiTable()并在控制台打印出包含菜品名称、单价、数量、总价、桌号与时间状态的明细清单。
结账买单
业务流程与规则校验
完成就餐消费结算,形成业务闭环:
未结校验:检查目标餐桌是否存在状态为
'未结账'的流水;状态核销:将该桌所有未结账单状态更新为指定支付方式(
现金/微信/支付宝);释放餐桌:将餐桌状态重置为
'空闲',并清空客户预订姓名与电话。

结账业务实现
// 业务层 BillService.java (结账部分)
public boolean hasUnpaidBill(int diningTableId) {
String sql = "SELECT * FROM bill WHERE diningTableId = ? AND state = '未结账' LIMIT 1";
return billDAO.querySingle(sql, Bill.class, diningTableId) != null;
}
public boolean payBill(int diningTableId, String payMode) {
// 1. 更新该桌所有未结账单
String sql = "UPDATE bill SET state = ? WHERE diningTableId = ? AND state = '未结账'";
int affected = billDAO.update(sql, payMode, diningTableId);
if (affected <= 0) return false;
// 2. 释放餐桌
return diningTableService.updateDiningTableToFree(diningTableId);
}事务演进
数据一致性与 ThreadLocal 事务
在“点餐(写账单 + 改桌态)”和“结账(核销账单 + 释放餐桌)”两个核心场景中,涉及多条 SQL 操作。若使用不同数据库连接,中途发生异常将造成脏数据(例如:账单写入成功但餐桌状态仍为空闲)。
- 进阶改造方案:
- 在
JDBCUtilsByDruid中引入ThreadLocal<Connection>,使同一个线程在一次业务调用中共享同一个数据库连接; - 在 Service 层调用
connection.setAutoCommit(false)开启显式事务; - 业务全部执行完毕后
commit();若捕获异常则统一rollback(),保证操作的原子性(Atomicity)。
- 在
架构重构路线图
| 演进阶段 | 技术栈 | 核心改造与收益 |
|---|---|---|
| 阶段 1:Web 化改造 | Servlet + JSP + Tomcat | 摆脱控制台黑白窗口,实现基于浏览器的可视化点餐与餐桌平面图监控 |
| 阶段 2:框架整合 (SSM) | Spring + Spring MVC + MyBatis | 引入 IoC 容器解耦组件,利用 AOP 实现声明式 @Transactional 事务,用 MyBatis XML 动态 SQL 替代手写 DBUtils |
| 阶段 3:前后端分离云原生 | Spring Boot 3 + Vue 3 + Redis | 前后端分离架构,Redis 缓存热门菜品数据,Spring Security + JWT 支撑基于 RBAC 的动态员工权限控制 |
附录:数据脚本
MySQL 初始化脚本
-- 1. 创建数据库
CREATE DATABASE IF NOT EXISTS `mhl` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE `mhl`;
-- 2. 员工表
DROP TABLE IF EXISTS `employee`;
CREATE TABLE `employee` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`empId` VARCHAR(50) NOT NULL UNIQUE,
`pwd` CHAR(32) NOT NULL,
`name` VARCHAR(50) NOT NULL,
`job` VARCHAR(50) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 3. 餐桌表
DROP TABLE IF EXISTS `diningTable`;
CREATE TABLE `diningTable` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`state` VARCHAR(20) NOT NULL DEFAULT '空闲',
`orderName` VARCHAR(50) NOT NULL DEFAULT '',
`orderTel` VARCHAR(20) NOT NULL DEFAULT ''
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 4. 菜谱表
DROP TABLE IF EXISTS `menu`;
CREATE TABLE `menu` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL,
`type` VARCHAR(50) NOT NULL,
`price` DOUBLE(10,2) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 5. 账单表
DROP TABLE IF EXISTS `bill`;
CREATE TABLE `bill` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`billId` VARCHAR(64) NOT NULL,
`menuId` INT NOT NULL,
`nums` INT NOT NULL,
`money` DOUBLE(10,2) NOT NULL,
`diningTableId` INT NOT NULL,
`billDate` DATETIME NOT NULL,
`state` VARCHAR(50) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 6. 初始化种子数据
-- 员工默认密码:666666 (MD5: f379eaf32cedb6307f66cfce04b829ac), 123456 (MD5: e10adc3949ba59abbe56e057f20f883e)
INSERT INTO `employee` (`empId`, `pwd`, `name`, `job`) VALUES
('6666', MD5('666666'), '韩顺平', '大堂经理'),
('0001', MD5('123456'), '张三', '主厨'),
('0002', MD5('123456'), '李四', '前台接待');
INSERT INTO `diningTable` (`id`, `state`) VALUES
(1, '空闲'), (2, '空闲'), (3, '空闲'), (4, '空闲'), (5, '空闲');
INSERT INTO `menu` (`name`, `type`, `price`) VALUES
('佛跳墙', '特色招牌', 198.00),
('东坡肉', '经典热菜', 58.00),
('西湖醋鱼', '经典热菜', 68.00),
('宫保鸡丁', '川味热菜', 38.00),
('松鼠鳜鱼', '苏帮名菜', 88.00),
('扬州炒饭', '主食精选', 22.00),
('西湖牛肉羹', '经典靓汤', 32.00);